/***
 * ASM: a very small and fast Java bytecode manipulation framework Copyright (c) 2000-2011 INRIA,
 * France Telecom All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted
 * provided that the following conditions are met: 1. Redistributions of source code must retain the
 * above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions
 * in binary form must reproduce the above copyright notice, this list of conditions and the
 * following disclaimer in the documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of its contributors may be used to
 * endorse or promote products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.googlecode.aviator.asm.commons;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.googlecode.aviator.asm.Handle;
import com.googlecode.aviator.asm.Label;
import com.googlecode.aviator.asm.MethodVisitor;
import com.googlecode.aviator.asm.Opcodes;
import com.googlecode.aviator.asm.Type;

/**
 * A {@link com.googlecode.aviator.asm.MethodVisitor} to insert before, after and around advices in
 * methods and constructors.
 * <p>
 * The behavior for constructors is like this:
 * <ol>
 *
 * <li>as long as the INVOKESPECIAL for the object initialization has not been reached, every
 * bytecode instruction is dispatched in the ctor code visitor</li>
 *
 * <li>when this one is reached, it is only added in the ctor code visitor and a JP invoke is
 * added</li>
 *
 * <li>after that, only the other code visitor receives the instructions</li>
 *
 * </ol>
 *
 * @author Eugene Kuleshov
 * @author Eric Bruneton
 */
public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {

  private static final Object THIS = new Object();

  private static final Object OTHER = new Object();

  protected int methodAccess;

  protected String methodDesc;

  private boolean constructor;

  private boolean superInitialized;

  private List<Object> stackFrame;

  private Map<Label, List<Object>> branches;

  /**
   * Creates a new {@link AdviceAdapter}.
   *
   * @param api the ASM API version implemented by this visitor. Must be one of
   *        {@link Opcodes#ASM4}.
   * @param mv the method visitor to which this adapter delegates calls.
   * @param access the method's access flags (see {@link Opcodes}).
   * @param name the method's name.
   * @param desc the method's descriptor (see {@link Type Type}).
   */
  protected AdviceAdapter(final int api, final MethodVisitor mv, final int access,
      final String name, final String desc) {
    super(api, mv, access, name, desc);
    methodAccess = access;
    methodDesc = desc;
    constructor = "<init>".equals(name);
  }

  @Override
  public void visitCode() {
    mv.visitCode();
    if (constructor) {
      stackFrame = new ArrayList<Object>();
      branches = new HashMap<Label, List<Object>>();
    } else {
      superInitialized = true;
      onMethodEnter();
    }
  }

  @Override
  public void visitLabel(final Label label) {
    mv.visitLabel(label);
    if (constructor && branches != null) {
      List<Object> frame = branches.get(label);
      if (frame != null) {
        stackFrame = frame;
        branches.remove(label);
      }
    }
  }

  @Override
  public void visitInsn(final int opcode) {
    if (constructor) {
      int s;
      switch (opcode) {
        case RETURN: // empty stack
          onMethodExit(opcode);
          break;
        case IRETURN: // 1 before n/a after
        case FRETURN: // 1 before n/a after
        case ARETURN: // 1 before n/a after
        case ATHROW: // 1 before n/a after
          popValue();
          onMethodExit(opcode);
          break;
        case LRETURN: // 2 before n/a after
        case DRETURN: // 2 before n/a after
          popValue();
          popValue();
          onMethodExit(opcode);
          break;
        case NOP:
        case LALOAD: // remove 2 add 2
        case DALOAD: // remove 2 add 2
        case LNEG:
        case DNEG:
        case FNEG:
        case INEG:
        case L2D:
        case D2L:
        case F2I:
        case I2B:
        case I2C:
        case I2S:
        case I2F:
        case ARRAYLENGTH:
          break;
        case ACONST_NULL:
        case ICONST_M1:
        case ICONST_0:
        case ICONST_1:
        case ICONST_2:
        case ICONST_3:
        case ICONST_4:
        case ICONST_5:
        case FCONST_0:
        case FCONST_1:
        case FCONST_2:
        case F2L: // 1 before 2 after
        case F2D:
        case I2L:
        case I2D:
          pushValue(OTHER);
          break;
        case LCONST_0:
        case LCONST_1:
        case DCONST_0:
        case DCONST_1:
          pushValue(OTHER);
          pushValue(OTHER);
          break;
        case IALOAD: // remove 2 add 1
        case FALOAD: // remove 2 add 1
        case AALOAD: // remove 2 add 1
        case BALOAD: // remove 2 add 1
        case CALOAD: // remove 2 add 1
        case SALOAD: // remove 2 add 1
        case POP:
        case IADD:
        case FADD:
        case ISUB:
        case LSHL: // 3 before 2 after
        case LSHR: // 3 before 2 after
        case LUSHR: // 3 before 2 after
        case L2I: // 2 before 1 after
        case L2F: // 2 before 1 after
        case D2I: // 2 before 1 after
        case D2F: // 2 before 1 after
        case FSUB:
        case FMUL:
        case FDIV:
        case FREM:
        case FCMPL: // 2 before 1 after
        case FCMPG: // 2 before 1 after
        case IMUL:
        case IDIV:
        case IREM:
        case ISHL:
        case ISHR:
        case IUSHR:
        case IAND:
        case IOR:
        case IXOR:
        case MONITORENTER:
        case MONITOREXIT:
          popValue();
          break;
        case POP2:
        case LSUB:
        case LMUL:
        case LDIV:
        case LREM:
        case LADD:
        case LAND:
        case LOR:
        case LXOR:
        case DADD:
        case DMUL:
        case DSUB:
        case DDIV:
        case DREM:
          popValue();
          popValue();
          break;
        case IASTORE:
        case FASTORE:
        case AASTORE:
        case BASTORE:
        case CASTORE:
        case SASTORE:
        case LCMP: // 4 before 1 after
        case DCMPL:
        case DCMPG:
          popValue();
          popValue();
          popValue();
          break;
        case LASTORE:
        case DASTORE:
          popValue();
          popValue();
          popValue();
          popValue();
          break;
        case DUP:
          pushValue(peekValue());
          break;
        case DUP_X1:
          s = stackFrame.size();
          stackFrame.add(s - 2, stackFrame.get(s - 1));
          break;
        case DUP_X2:
          s = stackFrame.size();
          stackFrame.add(s - 3, stackFrame.get(s - 1));
          break;
        case DUP2:
          s = stackFrame.size();
          stackFrame.add(s - 2, stackFrame.get(s - 1));
          stackFrame.add(s - 2, stackFrame.get(s - 1));
          break;
        case DUP2_X1:
          s = stackFrame.size();
          stackFrame.add(s - 3, stackFrame.get(s - 1));
          stackFrame.add(s - 3, stackFrame.get(s - 1));
          break;
        case DUP2_X2:
          s = stackFrame.size();
          stackFrame.add(s - 4, stackFrame.get(s - 1));
          stackFrame.add(s - 4, stackFrame.get(s - 1));
          break;
        case SWAP:
          s = stackFrame.size();
          stackFrame.add(s - 2, stackFrame.get(s - 1));
          stackFrame.remove(s);
          break;
      }
    } else {
      switch (opcode) {
        case RETURN:
        case IRETURN:
        case FRETURN:
        case ARETURN:
        case LRETURN:
        case DRETURN:
        case ATHROW:
          onMethodExit(opcode);
          break;
      }
    }
    mv.visitInsn(opcode);
  }

  @Override
  public void visitVarInsn(final int opcode, final int var) {
    super.visitVarInsn(opcode, var);
    if (constructor) {
      switch (opcode) {
        case ILOAD:
        case FLOAD:
          pushValue(OTHER);
          break;
        case LLOAD:
        case DLOAD:
          pushValue(OTHER);
          pushValue(OTHER);
          break;
        case ALOAD:
          pushValue(var == 0 ? THIS : OTHER);
          break;
        case ASTORE:
        case ISTORE:
        case FSTORE:
          popValue();
          break;
        case LSTORE:
        case DSTORE:
          popValue();
          popValue();
          break;
      }
    }
  }

  @Override
  public void visitFieldInsn(final int opcode, final String owner, final String name,
      final String desc) {
    mv.visitFieldInsn(opcode, owner, name, desc);
    if (constructor) {
      char c = desc.charAt(0);
      boolean longOrDouble = c == 'J' || c == 'D';
      switch (opcode) {
        case GETSTATIC:
          pushValue(OTHER);
          if (longOrDouble) {
            pushValue(OTHER);
          }
          break;
        case PUTSTATIC:
          popValue();
          if (longOrDouble) {
            popValue();
          }
          break;
        case PUTFIELD:
          popValue();
          if (longOrDouble) {
            popValue();
            popValue();
          }
          break;
        // case GETFIELD:
        default:
          if (longOrDouble) {
            pushValue(OTHER);
          }
      }
    }
  }

  @Override
  public void visitIntInsn(final int opcode, final int operand) {
    mv.visitIntInsn(opcode, operand);
    if (constructor && opcode != NEWARRAY) {
      pushValue(OTHER);
    }
  }

  @Override
  public void visitLdcInsn(final Object cst) {
    mv.visitLdcInsn(cst);
    if (constructor) {
      pushValue(OTHER);
      if (cst instanceof Double || cst instanceof Long) {
        pushValue(OTHER);
      }
    }
  }

  @Override
  public void visitMultiANewArrayInsn(final String desc, final int dims) {
    mv.visitMultiANewArrayInsn(desc, dims);
    if (constructor) {
      for (int i = 0; i < dims; i++) {
        popValue();
      }
      pushValue(OTHER);
    }
  }

  @Override
  public void visitTypeInsn(final int opcode, final String type) {
    mv.visitTypeInsn(opcode, type);
    // ANEWARRAY, CHECKCAST or INSTANCEOF don't change stack
    if (constructor && opcode == NEW) {
      pushValue(OTHER);
    }
  }

  @Override
  public void visitMethodInsn(final int opcode, final String owner, final String name,
      final String desc) {
    mv.visitMethodInsn(opcode, owner, name, desc);
    if (constructor) {
      Type[] types = Type.getArgumentTypes(desc);
      for (int i = 0; i < types.length; i++) {
        popValue();
        if (types[i].getSize() == 2) {
          popValue();
        }
      }
      switch (opcode) {
        // case INVOKESTATIC:
        // break;
        case INVOKEINTERFACE:
        case INVOKEVIRTUAL:
          popValue(); // objectref
          break;
        case INVOKESPECIAL:
          Object type = popValue(); // objectref
          if (type == THIS && !superInitialized) {
            onMethodEnter();
            superInitialized = true;
            // once super has been initialized it is no longer
            // necessary to keep track of stack state
            constructor = false;
          }
          break;
      }

      Type returnType = Type.getReturnType(desc);
      if (returnType != Type.VOID_TYPE) {
        pushValue(OTHER);
        if (returnType.getSize() == 2) {
          pushValue(OTHER);
        }
      }
    }
  }

  @Override
  public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
    mv.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
    if (constructor) {
      Type[] types = Type.getArgumentTypes(desc);
      for (int i = 0; i < types.length; i++) {
        popValue();
        if (types[i].getSize() == 2) {
          popValue();
        }
      }

      Type returnType = Type.getReturnType(desc);
      if (returnType != Type.VOID_TYPE) {
        pushValue(OTHER);
        if (returnType.getSize() == 2) {
          pushValue(OTHER);
        }
      }
    }
  }

  @Override
  public void visitJumpInsn(final int opcode, final Label label) {
    mv.visitJumpInsn(opcode, label);
    if (constructor) {
      switch (opcode) {
        case IFEQ:
        case IFNE:
        case IFLT:
        case IFGE:
        case IFGT:
        case IFLE:
        case IFNULL:
        case IFNONNULL:
          popValue();
          break;
        case IF_ICMPEQ:
        case IF_ICMPNE:
        case IF_ICMPLT:
        case IF_ICMPGE:
        case IF_ICMPGT:
        case IF_ICMPLE:
        case IF_ACMPEQ:
        case IF_ACMPNE:
          popValue();
          popValue();
          break;
        case JSR:
          pushValue(OTHER);
          break;
      }
      addBranch(label);
    }
  }

  @Override
  public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) {
    mv.visitLookupSwitchInsn(dflt, keys, labels);
    if (constructor) {
      popValue();
      addBranches(dflt, labels);
    }
  }

  @Override
  public void visitTableSwitchInsn(final int min, final int max, final Label dflt,
      final Label... labels) {
    mv.visitTableSwitchInsn(min, max, dflt, labels);
    if (constructor) {
      popValue();
      addBranches(dflt, labels);
    }
  }

  @Override
  public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
    super.visitTryCatchBlock(start, end, handler, type);
    if (constructor && !branches.containsKey(handler)) {
      List<Object> stackFrame = new ArrayList<Object>();
      stackFrame.add(OTHER);
      branches.put(handler, stackFrame);
    }
  }

  private void addBranches(final Label dflt, final Label[] labels) {
    addBranch(dflt);
    for (int i = 0; i < labels.length; i++) {
      addBranch(labels[i]);
    }
  }

  private void addBranch(final Label label) {
    if (branches.containsKey(label)) {
      return;
    }
    branches.put(label, new ArrayList<Object>(stackFrame));
  }

  private Object popValue() {
    return stackFrame.remove(stackFrame.size() - 1);
  }

  private Object peekValue() {
    return stackFrame.get(stackFrame.size() - 1);
  }

  private void pushValue(final Object o) {
    stackFrame.add(o);
  }

  /**
   * Called at the beginning of the method or after super class class call in the constructor. <br>
   * <br>
   *
   * <i>Custom code can use or change all the local variables, but should not change state of the
   * stack.</i>
   */
  protected void onMethodEnter() {}

  /**
   * Called before explicit exit from the method using either return or throw. Top element on the
   * stack contains the return value or exception instance. For example:
   *
   * <pre>
   *   public void onMethodExit(int opcode) {
   *     if(opcode==RETURN) {
   *         visitInsn(ACONST_NULL);
   *     } else if(opcode==ARETURN || opcode==ATHROW) {
   *         dup();
   *     } else {
   *         if(opcode==LRETURN || opcode==DRETURN) {
   *             dup2();
   *         } else {
   *             dup();
   *         }
   *         box(Type.getReturnType(this.methodDesc));
   *     }
   *     visitIntInsn(SIPUSH, opcode);
   *     visitMethodInsn(INVOKESTATIC, owner, "onExit", "(Ljava/lang/Object;I)V");
   *   }
   *
   *   // an actual call back method
   *   public static void onExit(Object param, int opcode) {
   *     ...
   * </pre>
   *
   * <br>
   * <br>
   *
   * <i>Custom code can use or change all the local variables, but should not change state of the
   * stack.</i>
   *
   * @param opcode one of the RETURN, IRETURN, FRETURN, ARETURN, LRETURN, DRETURN or ATHROW
   *
   */
  protected void onMethodExit(int opcode) {}

  // TODO onException, onMethodCall
}
